0007. 动态类型 vs. 静态类型
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 🤔 JavaScript、TypeScript 分别是“动态类型”还是“静态类型”?
- 4. 🤔 “动态类型”是什么?
- 5. 🤔 “静态类型”是什么?
- 6. 🤔 “静态类型”都有哪些优点?
- 7. 🤔 "静态类型"都有哪些缺点?
- 8. 💻 demos.1 - 「动态类型的 JS」 vs. 「静态类型的 TS」
- 9. 💻 demos.2 - 不要过分高估 TS 的智能
- 10. 💻 demos.3 - IDE 的交互式推断行为(Editor Flow Type Inference)
1. 🎯 本节内容
- 动态类型
- 静态类型
- 单次推断(one-shot inference)
- IDE 的交互式推断行为(Editor Flow Type Inference)
2. 🫧 评价
- 理解动态类型和静态类型之间的区别。
- 笔记中记录的这些静态类型的优点,也正是大多数 JS 项目转型为 TS 的主要原因。
- 通过笔记中记录的 demos 来体验静态类型的 TS 的优势。
- 你可以相信 TSC 的编译结果,但是不要过分相信 IDE 的智能推断,IDE 的推断有可能是错误的。
3. 🤔 JavaScript、TypeScript 分别是“动态类型”还是“静态类型”?
- JS 是 动态类型 的语言
- TS 是 静态类型 的语言
- TypeScript 的主要功能是为 JavaScript 添加类型系统。
- 大家可能知道,JavaScript 语言本身就有一套自己的类型系统,比如数值 123 和字符串 Hello。但是,JavaScript 的类型系统的约束非常弱,而且没有使用限制,运算符可以接受各种类型的值。
- 在语法上,JavaScript 属于动态类型语言。正是因为存在这些动态变化,所以 JavaScript 的类型系统是动态的,不具有很强的约束性。这对于提前发现代码错误,非常不利。
- TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。TypeScript 的作用,就是为 JavaScript 引入这种静态类型特征。
4. 🤔 “动态类型”是什么?
- “动态类型”是指类型检查在运行时进行,变量的类型由其当前值决定,可在程序执行过程中改变。
- JS 示例:
// 例一
let x = 1
x = 'hello'
// 例二
let y = { foo: 1 }
delete y.foo
y.bar = 22
3
4
5
6
7
8
- 例一:变量
x声明时,值的类型是数值,但是后面可以改成字符串。所以,无法提前知道变量的类型是什么,也就是说,变量的类型是动态的。 - 例二:变量
y是一个对象,有一个属性foo,但是这个属性是可以删掉的,并且还可以新增其他属性。所以,对象有什么属性,这个属性还在不在,也是动态的,没法提前知道。 - 正是因为存在这些动态变化,所以 JavaScript 的类型系统是动态的,不具有很强的约束性。极度灵活是它的优势,缺点是很难提前发现代码因类型问题导致的一些低级错误。
5. 🤔 “静态类型”是什么?
- “静态类型”是指类型检查在编译时(或开发时)进行,变量的类型在声明时确定,通常不可更改。
- TS 示例:
// 例一
let x = 1
x = 'hello' // ❌ Type 'string' is not assignable to type 'number'.
// 例二
let y = { foo: 1 }
delete y.foo // ❌ The operand of a 'delete' operator must be optional.
y.bar = 2 // ❌ Property 'bar' does not exist on type '{ foo: number; }'.2
3
4
5
6
7
8
- 例一:报错是因为变量赋值时,TypeScript 已经推断确定了类型,后面就不允许再赋值为其他类型的值,即变量的类型是静态的。
- 例二:报错是因为对象的属性也是静态的,不允许随意增删。
- TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。
- TypeScript 的作用,就是为 JavaScript 引入这种静态类型特征。
6. 🤔 “静态类型”都有哪些优点?
- 静态类型的核心优点:
| 优点类别 | 核心价值 | 关键收益 |
|---|---|---|
| 静态分析 | 编译时发现类型错误 | 降低线上风险 |
| 错误检测 | 提前捕获拼写和类型错误 | 节省调试时间 |
| IDE 支持 | 语法提示和自动补全 | 提升开发效率 |
| 代码文档 | 类型即文档 | 降低理解成本 |
| 代码重构 | 降低重构风险 | 提高代码可维护性 |
- 这个表格提炼了静态类型系统(如 TypeScript)相比动态类型的核心优势。
6.1. 有利于代码的静态分析
- 有了静态类型,不必运行代码,就可以确定变量的类型,从而推断代码有没有错误。这就叫做代码的静态分析。
- 对于大型项目非常重要,单单在开发阶段运行静态检查,就可以发现很多问题,避免交付有问题的代码,大大降低了线上风险。
// JS
let x = 1
x = 'hello' // ✅
// 一开始 x 是 1,是数字类型
// 随后将其改为字符串类型的 'hello' 不会报错
// 这是因为在 JS 中,类型是动态的,上述做法是被允许的。2
3
4
5
6
7
// TS
let x = 1
x = 'hello' // ❌
// Type 'string' is not assignable to type 'number'.(2322)
// 一开始给变量 x 赋值为 1
// 此时 TS 会认为变量 x 的类型是一个 number 类型
// 然后重新给 x 赋值为字符串 'hello',会报错
// 这是因为在 TS 中,类型是静态的,类型确定后如果再随便改,那是会报错的。2
3
4
5
6
7
8
9
6.2. 有利于发现错误
- 由于每个值、每个变量、每个运算符都有严格的类型约束,TypeScript 就能轻松发现拼写错误、语义错误和方法调用错误,节省程序员的时间。
let obj = { message: '' }
console.log(obj.messege) // ❌
// Property 'messege' does not exist on type '{ message: string; }'. Did you mean 'message'?(2551)
// 上面示例中,不小心把 message 拼错了,写成 messege。
// JavaScript 遇到这种情况是不报错的。
// TypeScript 就会报错,指出没有定义过这个属性。
// 错误提示中甚至会纠正你,询问你是否是想要访问 message。2
3
4
5
6
7
8
const a = 0
const b = true
const result = a + b // ❌
// Operator '+' cannot be applied to types 'number' and 'boolean'.(2365)
// 上面示例是合法的 JavaScript 代码,但是没有意义
// 不应该将数值 a 与布尔值 b 相加
// TypeScript 就会直接报错,提示运算符 + 不能用于数值和布尔值的相加。
// 对于大多数类似这样"无意义"的运算,在 TS 中都能够在程序运行之前给予我们反馈。2
3
4
5
6
7
8
9
function hello() {
return 'hello world'
}
hello().find('hello') // ❌
// Property 'find' does not exist on type 'string'.(2339)
// hello() 返回的是一个字符串,TypeScript 发现字符串没有 find() 方法,所以报错了。
// 如果是 JavaScript,只有到运行阶段才会报错。
// 访问了一个不存在的成员,这在 JS 中是最为常见的一种报错。2
3
4
5
6
7
8
9
10
6.3. 更好的 IDE 支持,做到语法提示和自动补全
- IDE(集成开发环境,比如 VSCode)一般都会利用类型信息,提供语法提示功能(编辑器自动提示函数用法、参数等)和自动补全功能(只键入一部分的变量名或函数名,编辑器补全后面的部分)。
6.4. 提供了代码文档
- 类型信息可以部分替代代码文档,解释应该如何使用这些代码,熟练的开发者往往只看类型,就能大致推断代码的作用。借助类型信息,很多工具能够直接生成文档。
6.5. 有助于代码重构
- 修改他人的 JavaScript 代码,往往非常痛苦,项目越大越痛苦,因为不确定修改后是否会影响到其他部分的代码。
- 类型信息大大减轻了重构的成本。一般来说,只要函数或对象的参数和返回值保持类型不变,就能基本确定,重构后的代码也能正常运行。如果还有配套的单元测试,就完全可以放心重构。越是大型的、多人合作的项目,类型信息能够提供的帮助越大。
6.6. 小结
- TypeScript 有助于提高代码质量,保证代码安全,更适合用在大型的企业级项目。这就是为什么大量 JavaScript 项目转成 TypeScript 的原因。
7. 🤔 "静态类型"都有哪些缺点?
- 静态类型的核心缺点提炼:
| 缺点类别 | 核心问题 | 具体影响 |
|---|---|---|
| 灵活性限制 | 丧失动态类型的自由度 | 约束了编程的灵活性 |
| 工作量增加 | 需要编写类型声明 | 延长开发时间 |
| 学习成本 | 类型系统复杂 | 提高入门门槛 |
| 编译步骤 | 增加编译流程 | 增加构建复杂度(但基本无需担心编译时间过长的问题) |
| 兼容性问题 | JS 库适配不完善 | 存在使用障碍 |
- 这个表格总结了静态类型系统(如 TypeScript)相比动态类型的主要缺点。
7.1. 丧失了动态类型的代码灵活性
- 动态类型有非常高的灵活性,给予程序员很大的自由,静态类型将这些灵活性都剥夺了。
7.2. 增加了编程工作量
- 有了类型之后,程序员不仅需要编写功能,还需要编写类型声明,确保类型正确。这增加了不少工作量,有时会显著拖长项目的开发时间。
7.3. 更高的学习成本
- 类型系统通常比较复杂,要学习的东西更多,要求开发者付出更高的学习成本。
7.4. 引入了独立的编译步骤
- 原生的 JavaScript 代码,可以直接在 JavaScript 引擎运行。
- 添加类型系统以后,就多出了一个单独的编译步骤,检查类型是否正确,并将 TypeScript 代码转成 JavaScript 代码,这样才能运行。
几乎不用担心编译速度问题
- 速度这个问题影响不大,使用原生 JS 写的编译器 tsc 已经很快了,并且官方也说明在 TS v7 中会改为 go 编写的 tsc,速度还会再提升 10 ~ 100 倍。
7.5. 兼容性问题
- TypeScript 依赖 JavaScript 生态,需要用到很多外部模块。但是,过去大部分 JavaScript 项目都没有做 TypeScript 适配,虽然可以自己动手做适配,不过使用时难免还是会有一些兼容性问题。
7.6. 小结
- 总的来说,这些缺点使得 TypeScript 不一定适合那些小型的、短期的个人项目。
8. 💻 demos.1 - 「动态类型的 JS」 vs. 「静态类型的 TS」
通过一个 demo 来对比 静态的 TS、动态的 JS
同样的一个功能,分别使用 JS、TS 两套代码来实现,体会一下 JS 的劣势,以及 TS 的优势。暂不要求掌握 TS 版的代码,在后续介绍 TS 知识点时都会介绍到。
8.1. 需求描述
- 输入:
" hEllO WoRLd " - 输出:
"Hello World"
8.2. 解法

8.3. JS 版
通过这个小 demo 来认识 JS 语言自身的一些短板。
function getStr() {
if (Math.random() < 0.5) {
return ' hEllO WoRLd '
}
return 404
}
function parseStr(str) {
return str
.split(' ') // ④
.filter((it) => it)
.map((it) => it[0].touppercase() + it.subStr(1).toLowerCase()) // ③
.join(' ')
}
const stt = getStr() // ①
const result = parsestr(str) // ②
console.log('result:', result)
// 上述是 JS 实现的版本
// 含有一堆的错误的代码2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 函数
getStr用于模拟后端提供的接口,这个接口在正常情况下,会返回一个字符串,否则会返回数字 404 - 函数
parseStr用于解析传入的字符串,按照需求处理传入的字符串,最终将期望的结果返回
错误分析
上面这段程序中存在很多错误,下面逐一分析:
- 变量名写错:
- ① 使用的是
stt - ② 使用的是
str
- ① 使用的是
- 函数名写错:
parsestr应该改为parseStr
- 字符串原型默认方法写错:
touppercase❌toUpperCase✅subStr❌substr或substring✅
- 将不确定的类型视作一个确定的类型来处理:
getStr返回的不一定是字符串,也有可能是数字,而数字身上哪来的split方法呢!这样的错误在 JS 中特常见,由于 JS 没法明确变量 myname 具体是什么类型。
为啥这些明显的错误,JS 无法识别出来呢?
因为 JS 是弱类型和解释型的脚本语言呀,这是 JS 语言的原罪,是没法直接通过 JS 自身来解决的。
回顾弱类型 & 解释型导致的隐患
再举个例子,有一个变量 a,在声明它时是 number 类型,但是我们后面却尝试去调用字符串的 api。
var a = 123
a.substr(1)2
对于这种情况,我们希望看到的是:在我们编写代码时就提供 ⚠️ 警告,尽快将这种错误解决掉。然而 JS 做不到,因为:
- 由于 JS 是弱类型的,当一个变量声明后,可以随时变更类型,它不知道我们从声明变量 a 到调用变量 a.substr 的过程中,是否中途又给变量 a 重新赋值了。中途给变量 a 重新赋值的这种情况是有可能发生的,这样 JS 就无法确定 a.substr 这么写是否是错误的。
- 由于 JS 是解释型的,错误发生的时间是在运行时,如果程序中发生了错误,只要不是低级的语法上的错误,那么这些错误都将在运行时才会抛出。所以,在将代码丢给宿主环境执行之前,它并不知道 a.substr 这么写是否是错误的。
var a = 123
// ... 假设这里很多代码
a = '123'
// ... 假设这里很多代码
a.substr(1)2
3
4
5
前端开发中,不少时间都是在排一些很 low 的类型错误。项目的规模越大,这些低级的错误问题就越明显。但是,这些问题在其它语言,比如 Java 中,是不存在的,一旦我们犯了类似上述这样的错误,它就会提醒我们出错了,但是在 js 中,它就没法提醒我们发生了错误。由于 js 是弱类型,解释型的语言,所以,这些问题是 js 无法解决的。而 TS 的出现,就是为了弥补 JS 在类型检查方面的短板,改善我们的开发体验,降低我们的开发成本(尤其是 debug)。
为什么说上述代码在执行的时候,有可能是 正常 的呢?
首先我们要知道,我们写的代码最终的运行环境是在宿主环境(比如"浏览器")中的。在宿主环境中,有可能会嵌入一些 script,我们不知道嵌入的 script 中是否会包含以下逻辑:
- 定义了 myname 变量
- 定义了 parseUsername 方法
- 扩展了字符串原型
String.prototype.touppercase、String.prototype.subStr - ......
简言之,咱们上述代码在执行的时候,有可能是没问题的。
8.4. TS 版
- 将
index.js的后缀给改为.ts,程序中的很多隐患会立刻暴露出来。 
- 根据提示,逐个修改,最终得到的 TS 代码如下
function getStr() {
if (Math.random() < 0.5) {
return ' hEllO WoRLd '
}
return 404
}
function parseStr(str: string) {
return str
.split(' ') // ④
.filter((it) => it)
.map((it) => it[0].toUpperCase() + it.substring(1).toLowerCase()) // ③
.join(' ')
}
const str: string | number = getStr() // ①
if (typeof str === 'string') {
const result = parseStr(str) // ②
console.log('result:', result)
}
// 将 index.js 的后缀给改为 .ts
// 程序中的很多隐患会立刻暴露出来
// 根据提示,逐个修改,最终得到的 TS 代码如上。2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"compilerOptions": {
"moduleDetection": "force",
"strict": true
}
}2
3
4
5
6
9. 💻 demos.2 - 不要过分高估 TS 的智能
- TS 的类型系统只能在一定程度上帮我们避免错误,但还是有一些错误是 TS 发现不了的,下面是一个示例:
// 示例 1
let arr: string[] = ['hello', 'typescript', 'world']
let foo = arr[3] // 1 正确
foo.toUpperCase() // 2 正确
// 1 - foo 被推断为了 string 类型,但是 arr[3] 是 undefined
// 2 - 实际运行的时候会抛出错误2
3
4
5
6
- 由于在 JS 中,数组成员数量是可以动态变化的,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。
- 但是我们也可以通过元组来解决数组动态长度的问题,比如:
// 示例 2
let arr: [string, string, string] = ['hello', 'typescript', 'world']
let foo = arr[3] // ❌
foo.toUpperCase() // ❌2
3
4

- 你会发现,示例 1 明明有错,但是 TS 没那么智能,它并没有提前预警,提醒我们有问题;但是示例 2 又可以正常提前提醒我们有错误。
- 这跟很多“JS 语言本身的技术债”有关,比如在接下来的学习中,我们还会经常接触到 undefined、null 这些特殊值,对于这些特殊值的处理,有 N 多细节,说实话,很难全都记住(好在是否记住这些类型层面的细节,对日常正常开发几乎没有影响),最好的做法是尝试去理解 TS 为何如此设计。
10. 💻 demos.3 - IDE 的交互式推断行为(Editor Flow Type Inference)
⚠️ 注意:TS 语言服务提供的 IDE 提示具有欺骗性!
- 你可以相信 tsc 的行为,但是尽量不要过分相信 IDE 提供的智能提示,它有时候是会出错的。
- 下面记录的错误或许会在后续的 TS 版本中进行修复,如果你也遇到了类似的错误提示,知道是怎么一回事即可。
下面我们来看一个示例:
const arr = []
// IDE 提示:
// const arr: any[]
arr.push(123) // 丢一个数字进去
type ArrType = typeof arr
// IDE 提示:
// type ArrType = number[]
arr.push('abc') // 丢一个字符串进去
type ArrType2 = typeof arr
// IDE 提示:
// type ArrType2 = (string | number)[]
const test1: ArrType = [1, '1']
// IDE 报错:
// Type 'string' is not assignable to type 'number'.(2322)
const test2: ArrType2 = [1, '1']2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 验证:你可以将上述示例复制到 TS Playground 中查看 TS 的类型推断结果来验证。



通过上述示例,你可以会得出下面这样一个 ❌ 错误的结论
=> 随着程序的执行 arr 的类型会不断变化,这一现象告诉我们,TS 对变量的类型推断也是动态的,TS 也并非严格意义上的静态类型。
- IDE 之所以会提示你 arr 的类型不断发生变化,实际上这是 IDE 提示的局限性导致的。
- 这是 IDE 的交互式推断行为(Editor Flow Type Inference),而非真正的 TypeScript 编译器推断结果。
- 在 IDE 中,编辑器会根据你代码的执行顺序(从上往下)做出上下文相关的增量推断,以便提示更符合你眼前的上下文,而 tsc 编译器 则是一次性建立整个语法树和符号表后,再统一推断所有类型。所以 arr 在编译器的静态视角中始终是
const arr: any[]。
- 验证:当你在本地新建一个
1.ts模块,然后将上述程序复制进去,再使用tsc 1.ts进行编译,会发现并不是报类型错误,这就表示 arr 的类型只被推断了一次,就是开始的any[]类型。如果类型真如 IDE 提示描述的那样,那么const test1: ArrType = [1, '1']这条报错的语句将抛出类型错误,导致编译无法通过。
const arr = []
// IDE 提示:
// const arr: any[]
arr.push(123) // 丢一个数字进去
type ArrType = typeof arr
// IDE 提示:
// type ArrType = number[]
arr.push('abc') // 丢一个字符串进去
type ArrType2 = typeof arr
// IDE 提示:
// type ArrType2 = (string | number)[]
const test1: ArrType = [1, '1']
// IDE 报错:
// Type 'string' is not assignable to type 'number'.(2322)
const test2: ArrType2 = [1, '1']2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const arr: number[] = []
type ArrType = typeof arr
const newArr: ArrType = [1, '1']
// IDE 报错:
// Type 'string' is not assignable to type 'number'.(2322)2
3
4
5
6
7
{
"name": "1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"compile_1": "tsc ./1.ts",
"compile_2": "tsc ./2.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.17.1",
"devDependencies": {
"typescript": "^5.9.3"
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.ts中的内容,就是最开始的示例源码2.ts中的内容,是刻意制造的一个类型错误 democompile_1会执行tsc ./1.ts对1.ts进行编译 -> 结果不会报错 -> 说明 IDE 报错是误报compile_2会执行tsc ./2.ts对2.ts进行编译 -> 错误 -> 说明 IDE 提示的错误是正确的

💡 提示://@ts-ignore
如果你觉得 IDE 提示的错误很烦,不想看到“错误”的爆红的话,解决办法也非常简单,在这一行前边儿加一个 //@ts-ignore 注释即可。

结论:
- TypeScript 的类型推断是静态的、单次推断(one-shot inference)。
- IDE(比如 TS Playground、VSCode 等)的类型推断提示具有局限性,可能会误报。
- 方案 1:使用注释指令,比如
//@ts-ignore、//@ts-nocheck注释来忽略误报错误;或者通过配置文件exclude忽略指定模块的检查;(主动) - 方案 2:当做看不见 🙈,等官方修复;(被动)
- 方案 1:使用注释指令,比如